#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import json
import os

from gaussdbt.commons.const import log, NormalErr, RoachConstant
from gaussdbt.commons.decorator_common import function_debug_log
from gaussdbt.commons.models import NodeInfo
from gaussdbt.resource.gaussdbt_resource import GaussCluster
from common.common import execute_cmd, execute_cmd_list, write_content_to_file
from common.const import RoleType, CMDResult, RepositoryDataTypeEnum, EnumPathType
from common.file_common import check_path_validity, check_file_or_dir, change_path_permission
from common.util.exec_utils import exec_mount_cmd, exec_umount_cmd, su_exec_rm_cmd, check_path_valid, \
    exec_overwrite_file, su_exec_cmd_list, ExecFuncParam


def check_path_validity_upper(path):
    upper_path = os.path.dirname(path)
    return check_path_validity(upper_path)


def check_backpy_key(backpy_key):
    if not isinstance(backpy_key, str):
        return False
    tmp_list = backpy_key.split('_')
    # backup_key期望值为20230627_102907
    if len(tmp_list) != 2:
        return False
    for str_ in tmp_list:
        if not str_.isdigit():
            return False
    return True


@function_debug_log
def check_uid_consistency(os_username, pid):
    """
    检查环境信息：集群的节点数量或者版本变化
    :return:
    """
    hostname = GaussCluster.get_hostname()
    all_nodes = GaussCluster.get_all_node()
    user_id = str(GaussCluster.get_user_info(os_username)[0])
    if not all_nodes:
        log.error("Node information is empty!")
        return False
    for node in all_nodes:
        if node.node_name != hostname:
            cmd = f"su - {os_username} -c 'ssh {node.node_name} \"grep '^{os_username}:' /etc/passwd | cut -d: -f3\"'"
            return_code, out_info, err_info = execute_cmd(cmd)
            if return_code != CMDResult.SUCCESS:
                log.error(f"Failed to execute the check uid consistency command, out:{out_info}, job_id: {pid}.")

                raise Exception("Execute check uid consistency cmd Failed!")
            output = out_info.split('\n')
            log.info(f"check_uid_consistency output:{output}")
            if user_id not in output:
                log.info(f"User {os_username} uid is not consistent, {hostname}:{user_id}, {node.node_name}:{output}.")
                return False
    log.info(f"User {os_username} uid is consistent, uid: {user_id}.")
    return True


@function_debug_log
def check_env_info(backup_key, meta_repository):
    """
    检查环境信息：集群的节点数量或者版本变化
    :return:
    """
    if not check_path_validity(meta_repository):
        log.error(f"Meta repository: {meta_repository} is invalid")
        return False
    all_nodes = GaussCluster.get_all_node()
    gdb_version = GaussCluster.get_gdb_version()
    copy_metadata_path = os.path.join(meta_repository, "meta", backup_key)
    backup_meta_info_path = os.path.join(copy_metadata_path, "metadata.json")
    with open(backup_meta_info_path) as file_temp:
        meta_info = json.loads(file_temp.read())
    if "version" not in meta_info:
        log.error(f"Copy: {backup_key} meta info is invalid no version key ")
        return False
    copy_database_version = meta_info.get("version")
    if gdb_version != copy_database_version:
        log.error(f"GaussDB version have changed now is {gdb_version} copy is {copy_database_version}")
        return False
    current_primary_num, current_standby_num = computer_node_num(all_nodes)
    if "nodeInfo" not in meta_info:
        log.error(f"Copy: {backup_key} meta info is invalid no nodeInfo key ")
        return False
    nodes_info = meta_info.get("nodeInfo")
    if len(all_nodes) != len(nodes_info):
        log.error(f"GaussDB node number is different now is {len(all_nodes)}, copy is {len(nodes_info)}")
        return False
    primary_num, standby_num = computer_node_num(nodes_info)
    if primary_num != current_primary_num:
        log.error(
            f"GaussDB primary node number is different now is {len(current_primary_num)}, copy is {len(primary_num)}")
        return False
    if standby_num != current_standby_num:
        log.error(
            f"GaussDB standby node number is different now is {len(current_standby_num)}, copy is {len(standby_num)}")
        return False
    return True


def computer_node_num(nodes_info):
    primary_num = 0
    standby_num = 0
    for node in nodes_info:
        item = node
        if isinstance(node, dict):
            if "node_role" not in node:
                log.error(f"Node info is invalid no node_role key ")
                continue
            item = NodeInfo(node_role=node.get("node_role"))
        if int(item.node_role) == RoleType.PRIMARY:
            primary_num = primary_num + 1
        elif int(item.node_role) == RoleType.STANDBY:
            standby_num = standby_num + 1
    return primary_num, standby_num


@function_debug_log
def save_full_backup_key(file_path, backup_key):
    """
    保存全量副本的backup key 到指定路径
    :param file_path: str
    :param backup_key: str
    :return: bool
    """
    if not check_path_validity(file_path):
        log.error(f"File path:{file_path} is invalid")
        return False
    file_name = os.path.join(file_path, "LastFullCopyKey")
    if os.path.exists(file_name):
        log.debug(f"Full backup key file: {file_name} exist and remove")
        os.remove(file_name)
    write_content_to_file(file_name, backup_key)
    return True


@function_debug_log
def get_latest_full_backup_key(file_path):
    """
    获取最近的全量副本的backup key
    :param file_path: str
    :return: str
    """
    if not check_path_validity(file_path):
        log.error(f"File path: {file_path} is invalid")
        return ""
    file_name = os.path.join(file_path, "LastFullCopyKey")
    if not os.path.exists(file_name):
        log.error(f"LastFullCopyKey file: {file_name} not exist")
        return ""
    with open(file_name, "r") as file:
        backup_key = file.read()
    return backup_key


@function_debug_log
def get_latest_diff_backup_key(file_path):
    if not check_path_validity(file_path):
        log.error(f"File path:{file_path} is invalid")
        return ""
    file_name = os.path.join(file_path, "LastDiffCopyKey")
    if not os.path.exists(file_name):
        log.error(f"LastDiffCopyKey file: {file_name} not exist")
        return ""
    with open(file_name, "r") as file:
        backup_key = file.read()
    return backup_key


@function_debug_log
def check_mount_bind_point(mount_point):
    """
    检测挂载点是否有其他文件路径 mount bind
    :param mount_point: str
    :return: bool
    """
    if not check_path_validity(mount_point):
        log.error(f"Mount point: {mount_point} is invalid")
        return False
    check_cmd = ["mount", f"grep {mount_point}"]
    return_code, _, std_err = execute_cmd_list(check_cmd)
    if return_code != CMDResult.SUCCESS:
        log.warning(f"Mount point have not found error {std_err}")
        return False
    log.debug("Mount point have found")
    return True


@function_debug_log
def umount_bind_path(mount_point):
    """
    去挂载指定挂载点
    :param mount_point: str
    :return: bool
    """
    if not check_path_validity(mount_point):
        log.error(f"Mount point: {mount_point} is invalid")
        return False
    return_code, _, std_err = exec_umount_cmd(mount_point)
    if return_code != CMDResult.SUCCESS:
        log.warning(f"Failed to exec umount bind path, err: {std_err}")
        return False
    log.info("Succeed to exec umount bind path")
    return True


@function_debug_log
def mount_bind_path(src_area, des_area):
    """
    src_area 挂载绑定到指定的 des_area
    :param src_area: str
    :param des_area: str
    :return: bool
    """
    if not check_path_validity(src_area) or not check_path_validity(des_area):
        log.error(f"Src path: {src_area} or des path: {des_area} is invalid")
        return False
    result = check_mount_bind_point(des_area)
    if result and not umount_bind_path(des_area):
        log.error(f"Umount bind path:{des_area} failed")
        return False
    if not os.path.isdir(des_area):
        try:
            os.makedirs(des_area, 0o777, True)
        except Exception as ex:
            log.error(f"Create dir {des_area} failed exception {ex}")
            return False
    return_code, _, std_err = exec_mount_cmd(src_area, des_area)
    if return_code != CMDResult.SUCCESS:
        log.error(f"Failed to exec mount bind path, err: {std_err}")
        return False
    return True


def record_err_code(errcode: int, file_name: str, user_name: str):
    log.info('Write errcode')
    if not user_name:
        log.error("Get user name err")
        return False
    if os.path.islink(file_name):
        log.error("Include link path")
        return False
    exec_overwrite_file(file_name, str(errcode), json_flag=False)
    change_path_permission(file_name, user_name)
    return True


def query_err_code(code_file: str, user_name: str):
    """
    解析错误码
    :return:
    """
    log.info('Query err code!')
    if not user_name:
        log.error("Get user name err")
        return NormalErr.FALSE
    if os.path.islink(code_file):
        log.error("Include link path")
        return NormalErr.FALSE
    code = NormalErr.WAITING
    log.info(f'{code_file}')
    if not os.path.exists(code_file):
        log.info(f"Code file: {code_file} not exist, maybe running, please wait.")
        return code
    log.info(f'Code path exist')
    with open(code_file, "r", encoding='UTF-8') as file:
        err_code = file.read().strip()
    su_exec_rm_cmd(code_file)
    return int(err_code)


def parse_backup_path(repositories: list):
    """
    解析数据仓信息
    :param repositories:
    :return:
    """
    backup_dirs = dict()
    backup_dirs["meta_repository"] = []
    for repository in repositories:
        repository_type = repository.get("repositoryType")
        if repository_type == RepositoryDataTypeEnum.DATA_REPOSITORY:
            backup_dirs["data_repository"] = (repository.get("path"))
        elif repository_type == RepositoryDataTypeEnum.CACHE_REPOSITORY:
            backup_dirs["cache_repository"] = (repository.get("path"))
        elif repository_type == RepositoryDataTypeEnum.LOG_REPOSITORY:
            backup_dirs["log_repository"] = repository.get("path")
        elif repository_type == RepositoryDataTypeEnum.META_REPOSITORY:
            backup_dirs["meta_repository"].extend(repository.get("path"))
    return backup_dirs


def record_gdb_err_code(errcode: int, file_name: str):
    log.info(f'Write errcode')
    if os.path.islink(file_name):
        log.error(f"Link file:{file_name},stop writing.")
        return False
    exec_overwrite_file(file_name, str(errcode), json_flag=False)
    return True


def query_gdb_err_code(code_file: str):
    """
    解析错误码
    :return:
    """
    log.info('Query err code!')
    code = NormalErr.WAITING
    log.info(f'{code_file}')
    if os.path.islink(code_file):
        log.error(f"Link file:{code_file},stop writing.")
        return NormalErr.FALSE
    if not os.path.exists(code_file):
        log.info(f"Code file: {code_file} not exist, maybe running, please wait.")
        return code
    log.info(f'Code path exist')
    with open(code_file, "r", encoding='UTF-8') as code_file_obj:
        pre_job_err = code_file_obj.read().strip()
    if check_path_valid(code_file):
        os.remove(code_file)
    try:
        int_pre_job_err = int(pre_job_err)
    except Exception:
        log.error("Get code err!")
        return code
    return int_pre_job_err


def check_mount_point_use(mount_point: str):
    check_mount = f"fuser -m {mount_point}"
    return_code, std_out, std_err = execute_cmd(check_mount)
    if return_code != CMDResult.SUCCESS:
        log.error(f"Failed to exec check mount point failed, err: {std_err}")
        return False
    use_number = len(std_out.split(' '))
    if use_number < RoachConstant.MOUNT_BUSY_NUM:
        log.warning(f"Use mount point {mount_point} is busy use nunber is {use_number}")
        return True
    return False


def exec_chmod_dir_recursively(input_path: str, mode):
    """
    递归修改目录所属用户和用户组
    :param mode: chmod参数
    :param input_path: 待修改目录
    """
    path_type = check_file_or_dir(input_path)
    if path_type == EnumPathType.INVALID_TYPE:
        log.error(f"input path: {input_path} is invalid can not exec chmod dir recursively")
        return False
    change_path_permission(input_path, mode=mode)
    for root, dirs, files in os.walk(input_path):
        for tmp_dir in dirs:
            tmp_dir_path = os.path.join(root, tmp_dir)
            change_path_permission(tmp_dir_path, mode=mode)
        for tmp_file in files:
            tmp_file = os.path.join(root, tmp_file)
            change_path_permission(tmp_file, mode=mode)
    return True
